package org.acooke.uykfd.db

import java.sql.Connection
import java.util.{Set => JSet}

import org.apache.empire.data.RecordData
import org.apache.empire.db.DBColumn
import org.apache.empire.db.DBJoinType
import org.apache.empire.db.DBReader

import org.jgrapht.alg.ConnectivityInspector
import org.jgrapht.alg.FloydWarshallShortestPaths
import org.jgrapht.Graph
import org.jgrapht.WeightedGraph
import org.jgrapht.graph.DefaultWeightedEdge
import org.jgrapht.graph.Multigraph
import org.jgrapht.graph.WeightedMultigraph
import org.jgrapht.graph.SimpleDirectedGraph
import org.jgrapht.graph.SimpleGraph
import org.jgrapht.graph.SimpleWeightedGraph
import org.jgrapht.UndirectedGraph
import org.jgrapht.DirectedGraph

import scala.collection.mutable.HashSet
import scala.collection.immutable.SortedSet
import scala.collection.immutable.TreeSet
import scala.collection.JavaConversions._


class EdgeException(msg: String) extends RuntimeException(msg)


trait EdgeRow[N <: IdRow]

trait EdgeTable[R <: EdgeRow[N], N <: IdRow, O <: Schema.Id[_], F, T <: Schema.Edges[F,O]]
  extends Table[F,T]

protected abstract class EdgeFactory[
    N <: IdRow, O <: Schema.Id[_], R <: EdgeRecord[N,O,R,F,T], F, T <: Schema.Edges[F,O]](
    table: => T, nt: O, nf: (Int, Connection) => N)
  extends Factory[R,F,T](table) {

  val nodeTable = nt
  def nodeFactory = nf

  def fromNodes(source: N, sink: N, cnxn: Connection) = fromNodeIds(source.id, sink.id, cnxn)

  def fromNodeIds(l: Int, r: Int, cnxn: Connection) = {
    val (source, sink) = orderIds(l, r)
    val cmd = all
    cmd.where(table.SOURCE.is(source))
    cmd.where(table.SINK.is(sink))
    val row = rowFactory
    row.pull(cmd, cnxn, (x: Exception) => row.pushNodeIds(source, sink, cnxn))
  }

  def lookupNodes(source: N, sink: N, cnxn: Connection) = lookupNodeIds(source.id, sink.id, cnxn)

  def lookupNodeIds(l: Int, r: Int, cnxn: Connection): Option[R] = {
    val (source, sink) = orderIds(l, r)
    val cmd = all
    cmd.where(table.SOURCE.is(source))
    cmd.where(table.SINK.is(sink))
    val row = rowFactory
    try {
      Some(row.pull(cmd, cnxn))
    } catch {
      case _ => None
    }
  }

  def fromNode(node: N, cnxn: Connection) = fromNodeId(node.id, cnxn)

  def fromNodeId(nodeId: Int, cnxn: Connection) =
    buildList(allWhere(table.SOURCE.is(nodeId).or(table.SINK.is(nodeId))), cnxn)

  def fromSink(sink: N, cnxn: Connection) = fromSinkId(sink.id, cnxn)

  def fromSinkId(sinkId: Int, cnxn: Connection) = fromWorker(sinkId, table.SINK, cnxn)

  def fromSource(source: N, cnxn: Connection) = fromSourceId(source.id, cnxn)

  def fromSourceId(sourceId: Int, cnxn: Connection) = fromWorker(sourceId, table.SOURCE, cnxn)

  def fromWorker(nodeId: Int, column: DBColumn, cnxn: Connection) = 
    buildList(allWhere(column.is(nodeId)), cnxn)

  // see Undirected
  protected def order(a: N, b: N): (N, N) = (a, b)
  protected def orderIds(a: Int, b: Int): (Int, Int) = (a, b)

  def fromGraph(graph: Graph[Int, DefaultWeightedEdge], cnxn: Connection) = {
    print("[")
    for (edge <- graph.edgeSet) {
      print("-")
      fromNodeIds(graph.getEdgeSource(edge), graph.getEdgeTarget(edge), cnxn)
    }
    println("]")
  }

  def toSimpleGraph(cnxn: Connection) =
    populateGraph[SimpleGraph[Int, DefaultWeightedEdge], Graph[Int, DefaultWeightedEdge]](
      new SimpleGraph[Int, DefaultWeightedEdge](classOf[DefaultWeightedEdge]),
      populateRow[Graph[Int, DefaultWeightedEdge]](false), cnxn)

  def toDirectedGraph(cnxn: Connection): SimpleDirectedGraph[Int, DefaultWeightedEdge] =
    populateGraph[SimpleDirectedGraph[Int, DefaultWeightedEdge], Graph[Int, DefaultWeightedEdge]](
      new SimpleDirectedGraph[Int, DefaultWeightedEdge](classOf[DefaultWeightedEdge]),
      populateRow[Graph[Int, DefaultWeightedEdge]](false), cnxn)

  def toMultigraph(cnxn: Connection) =
    populateGraph[Multigraph[Int, DefaultWeightedEdge], Graph[Int, DefaultWeightedEdge]](
      new Multigraph[Int, DefaultWeightedEdge](classOf[DefaultWeightedEdge]),
      populateRow[Graph[Int, DefaultWeightedEdge]](true), cnxn)

  def populateGraph[G <: I, I <: Graph[Int, DefaultWeightedEdge]](
    graph: G, doRow: (I, RecordData) => DefaultWeightedEdge, cnxn: Connection
  ): G = {
    print("[")
    val reader = new DBReader
    try {
      reader.open(all, cnxn)
      while (reader.moveNext) {
        print("+")
        doRow(graph, reader)
      }
      graph
    } finally {
      reader.close
      println("]")
    }
  }

  def populateRow[I <: Graph[Int, DefaultWeightedEdge]](multi: Boolean) = {
    (graph: Graph[Int, DefaultWeightedEdge], data: RecordData) => {
      val source = Record.asInt(data, table.SOURCE)
      val sink = Record.asInt(data, table.SINK)
      if (! graph.containsVertex(source)) graph.addVertex(source)
      if (! graph.containsVertex(sink)) graph.addVertex(sink)
      if (graph.containsEdge(source, sink) && ! multi)
        throw new EdgeException("" + source + " - " + sink + " already exists")
      graph.addEdge(source, sink)
    }
  }

  def sourceNeighbourIds(sink: N, cnxn: Connection) = 
    neighboursWorker[Int](sink, table.SINK, table.SOURCE, id, cnxn)

  def sourceNeighbours(sink: N, cnxn: Connection) = 
    neighboursWorker[N](sink, table.SINK, table.SOURCE, nodeFactory(_, cnxn), cnxn)
  
  def sinkNeighbourIds(source: N, cnxn: Connection) = 
    neighboursWorker[Int](source, table.SOURCE, table.SINK, id, cnxn)

  def sinkNeighbours(source: N, cnxn: Connection) = 
    neighboursWorker[N](source, table.SOURCE, table.SINK, nodeFactory(_, cnxn), cnxn)
  
  def neighboursWorker[X](
    node: N, us: DBColumn, them: DBColumn, factory: Int => X, cnxn: Connection
  ): List[X] = {
    val cmd = where(us.is(node.id))
    cmd.select(them)
    buildList(cmd, them, factory, cnxn)
  }
  
  def allNeighbourIds(node: N, cnxn: Connection) = 
    allNeighboursWorker[Int](node, id, cnxn)

  def allNeighbours(node: N, cnxn: Connection) = 
    allNeighboursWorker[N](node, nodeFactory(_, cnxn), cnxn)
  
  def allNeighboursWorker[X](node: N, factory: Int => X, cnxn: Connection): List[X] = {
    val nbr = "NBR"
    val source = where(table.SOURCE.is(node.id))
    source.select(table.SINK.as(nbr))
    val sink = where(table.SINK.is(node.id))
    sink.select(table.SOURCE.as(nbr))
    val cmd = source.union(sink)
    buildList(cmd, table.SINK.as(nbr), factory, cnxn)
  }

  def outDegree(node: N, cnxn: Connection) = {
    val cmd = where(table.SOURCE.is(node.id))
    cmd.select(table.count)
    Schema.querySingleInt(cmd.getSelect, 0, cnxn)
  }
      
  def inDegree(node: N, cnxn: Connection) = {
    val cmd = where(table.SINK.is(node.id))
    cmd.select(table.count)
    Schema.querySingleInt(cmd.getSelect, 0, cnxn)
  }
      
  def showConnected(graph: DirectedGraph[Int, _], display:(Int => String)) = {
    if (graph.vertexSet.size == 0) throw new Exception("Empty")
    val inspector = new ConnectivityInspector(graph)
    if (! inspector.isGraphConnected) {
      var biggest: JSet[Int] = null
      for (nodes <- inspector.connectedSets) {
        if (biggest == null) {
          biggest = nodes
        } else {
          var smaller = nodes
          if (biggest.size < smaller.size) {
            val tmp = biggest
            biggest = smaller
            smaller = tmp
          }
          println("\nIsolated nodes:")
          for (node <- smaller) {
            println(display(node))
          }
        }
      }
      false
    } else {
      true
    }
  }

  def unconnectedSourceNodes(cnxn: Connection) = {
    val cmd = Schema.createCommand
    cmd.select(nodeTable.ID)
    cmd.selectDistinct()
    cmd.join(nodeTable.ID, table.SOURCE, DBJoinType.LEFT)
    cmd.where(table.SOURCE.is(null))
    buildList(cmd, nodeTable.ID, nodeFactory(_, cnxn), cnxn)
  }

}

protected abstract class EdgeRecord[
    N <: IdRow, O <: Schema.Id[_], S <: EdgeRecord[N,O,S,F,T], F, T <: Schema.Edges[F,O]](
    table: => T, nf: (Int, Connection) => N)
  extends Record[S,F,T](table) {

  self: S =>

  def nodeFactory = nf

  def sourceId = asInt(table.SOURCE)
  def source(cnxn: Connection) = nodeFactory(sourceId, cnxn)
  def sinkId = asInt(table.SINK)
  def sink(cnxn: Connection) = nodeFactory(sinkId, cnxn)

  def pushNodes(source: N, sink: N, cnxn: Connection) =
    pushNodeIds(source.id, sink.id, cnxn)

  def pushNodeIds(sourceId: Int, sinkId: Int, cnxn: Connection) = {
    set(table.SOURCE, sourceId)
    push(table.SINK, sinkId, cnxn)
    this
  }

  def other(node: N, cnxn: Connection) =
    if (node.id == sourceId) sink(cnxn)
    else if (node.id == sinkId) source(cnxn)
    else throw new RuntimeException("Neither node is " + node)
}


trait WeightedEdgeRow[N <: IdRow] extends EdgeRow[N] {
  def weight: Int
}

trait WeightedEdgeTable[R <: WeightedEdgeRow[N], N <: IdRow, O <: Schema.Id[_], F, T <: Schema.WeightedEdges[F,O]]
  extends EdgeTable[R,N,O,F,T] {
  def maxWeight(cnxn: Connection): Option[Int]
}

protected abstract class WeightedEdgeFactory[
    N <: IdRow, O <: Schema.Id[_], R <: WeightedEdgeRecord[N,O,R,F,T], F, T <: Schema.WeightedEdges[F,O]](
    table: => T, nt: O, nf: (Int, Connection) => N)
  extends EdgeFactory[N,O,R,F,T](table, nt, nf) {

  def fromNodesWithWeight(l: N, default: Int, r: N, cnxn: Connection, replace: Boolean=true) = {
    val (source, sink) = order(l, r)
    val cmd = allWhere(table.SOURCE.is(source.id))
    // don't include weight in select
    cmd.where(table.SINK.is(sink.id))
    val row = rowFactory
    row.pull(cmd, cnxn, (x: Exception) => row.pushAll(source, default, sink, cnxn))
    if (replace && row.weight < default) row.pushWeight(default, cnxn)
    row
  }  

  def fromShortestPaths(
      graph: Graph[Int, DefaultWeightedEdge], 
      distances: FloydWarshallShortestPaths[Int, DefaultWeightedEdge], 
      calcWeight: (Double, Double) => Option[Int], cnxn: Connection) {
    erase(cnxn)
    applyShortestPaths(setEdge, graph, distances, calcWeight, cnxn)
  }

  private def setEdge(cnxn: Connection, source: Int, sink: Int, weight: Option[Int]) =
    weight match {
      case Some(weight) =>
        val cmd = Schema.createCommand
        cmd.set(table.SOURCE.to(source))
        cmd.set(table.WEIGHT.to(weight))
        cmd.set(table.SINK.to(sink))
        Schema.executeSQL(cmd.getInsert, cnxn)
      case _ => ()}

  private def addEdgeFactory(cnxn: Connection) = {
    val unconnected = maxWeight(cnxn) match {
      case Some(unconnected) => unconnected
      case None => 0
    }
    (cnxn: Connection, source: Int, sink: Int, weight: Option[Int]) => 
      weight match {
        case Some(weight) =>
          val initial = fromNodesWithWeight(
            nodeFactory(source, cnxn), unconnected, nodeFactory(sink, cnxn), cnxn, false)
          initial.pushWeight(initial.weight + weight, cnxn)
          ()
        case _ => ()}
  }

  def addShortestPaths(
      graph: Graph[Int, DefaultWeightedEdge], 
      distances: FloydWarshallShortestPaths[Int, DefaultWeightedEdge], 
      calcWeight: (Double, Double) => Option[Int], cnxn: Connection) {
    val addEdge = addEdgeFactory(cnxn)
    applyShortestPaths(addEdge, graph, distances, calcWeight, cnxn)
  }

  def applyShortestPaths(
      action: (Connection, Int, Int, Option[Int]) => Unit,
      graph: Graph[Int, DefaultWeightedEdge], 
      distances: FloydWarshallShortestPaths[Int, DefaultWeightedEdge], 
      calcWeight: (Double, Double) => Option[Int], cnxn: Connection) {
    print("[")
    val diameter = distances.getDiameter
    val size = graph.vertexSet.size
    val visited = new HashSet[(Int, Int)]
    for (l <- graph.vertexSet) {
      for (r <- graph.vertexSet) {
        print(".")
        val (source, sink) = orderIds(l, r)
        // this lets us handle both undirected and directed graphs
        if (! visited.contains((source, sink))) {
          action(cnxn, source, sink, 
                 calcWeight(distances.shortestDistance(source, sink), distances.getDiameter))
          visited.add((source, sink))
        }
      }
    }
    println("]")
  }

  def maxWeight(cnxn: Connection) = {
    val max = table.WEIGHT.max
    val cmd = Schema.createCommand
    cmd.select(max)
    try {
      val reader = new DBReader
      reader.getRecordData(cmd, cnxn)
      Some(Record.asInt(reader, max))
    } catch {
      case _ => None
    }
  }

  def toSimpleWeightedGraph(calcWeight: (Int) => Double, cnxn: Connection) =
    populateGraph[SimpleWeightedGraph[Int, DefaultWeightedEdge], WeightedGraph[Int, DefaultWeightedEdge]](
      new SimpleWeightedGraph[Int, DefaultWeightedEdge](classOf[DefaultWeightedEdge]),
      populateRow[WeightedGraph[Int, DefaultWeightedEdge]](false, calcWeight), cnxn)

  def toWeightedMultigraph(calcWeight: (Int) => Double, cnxn: Connection) =
    populateGraph[WeightedMultigraph[Int, DefaultWeightedEdge], WeightedGraph[Int, DefaultWeightedEdge]](
      new WeightedMultigraph[Int, DefaultWeightedEdge](classOf[DefaultWeightedEdge]),
      populateRow[WeightedGraph[Int, DefaultWeightedEdge]](true, calcWeight), cnxn)

  def populateRow[I <: WeightedGraph[Int, DefaultWeightedEdge]](multi: Boolean, calcWeight: (Int) => Double) = {
    val chain = super.populateRow(multi)
    (graph: I, data: RecordData) => {
      val edge = chain(graph, data)
      graph.setEdgeWeight(edge, calcWeight(Record.asInt(data, table.WEIGHT)))
      edge
    }
  }

  def allNeigbourIdsWithWeightAbove(nodeId: Int, minWeight: Int, cnxn: Connection) = {
    val nbr = "NBR"
    val source = where(table.SOURCE.is(nodeId))
    source.where(table.WEIGHT.isGreaterThan(minWeight))
    source.select(table.SINK.as(nbr), table.WEIGHT)
    val sink = where(table.SINK.is(nodeId))
    sink.where(table.WEIGHT.isGreaterThan(minWeight))
    sink.select(table.SOURCE.as(nbr), table.WEIGHT)
    val cmd = source.union(sink)
    buildList(cmd, table.SINK.as(nbr), id, cnxn)
  }

}

protected abstract class WeightedEdgeRecord[
    N <: IdRow, O <: Schema.Id[_], S <: WeightedEdgeRecord[N,O,S,F,T], F, T <: Schema.WeightedEdges[F,O]](
    table: => T, nf: (Int, Connection) => N)
  extends EdgeRecord[N,O,S,F,T](table, nf) {

  self: S =>

  def weight = asInt(table.WEIGHT)

  def pushAll(source: N, weight: Int, sink: N, cnxn: Connection) = 
    pushAllIds(source.id, weight, sink.id, cnxn)

  def pushAllIds(sourceId: Int, weight: Int, sinkId: Int, cnxn: Connection) = {
    set(table.SOURCE, sourceId)
    set(table.SINK, sinkId)
    pushWeight(weight, cnxn)
    this
  }

  def pushWeight(weight: Int, cnxn: Connection) = push(table.WEIGHT, weight, cnxn)

}


trait Undirected[N <: IdRow, O <: Schema.Id[_], R <: EdgeRecord[N,O,R,F,T], F, T <: Schema.Edges[F,O]]
  extends EdgeFactory[N,O,R,F,T] {
                                                                               
  override protected def order(a: N, b: N) = {
    if (a.id > b.id) {
      (a, b)
    } else {
      (b, a)
    }
  }

  override protected def orderIds(a: Int, b: Int) = {
    if (a > b) {
      (a, b)
    } else {
      (b, a)
    }
  }

  def degree(node: N, cnxn: Connection) =
    inDegree(node, cnxn) + outDegree(node, cnxn)
                                                                     
}

